AndroidのListViewを、iOSのUITableViewみたいにセクション毎に分けてみた。【12日目】
現在、とあるiOSアプリをAndroidに移植していますが、その作業の中ですごく不便に感じたのが、AndroidのListViewです。
iOSのUITableViewでは簡単にセクション毎に分けることができますが、AndroidのListViewではやや手間がかかるようです。
そこで今回は、Androidでも簡単にセクション毎に分けることができるアダプタークラスを作ってみました。
まず、セクションと行を保持する「IndexPath」クラスを作成します。これは、iOSの「NSIndexPath」の代わりのようなものです。
IndexPath.java
package jp.classmethod; public class IndexPath { public int section; public int row; }
そして次に、今回の本題となるBaseAdapterクラスを継承したアダプタークラス「BaseSectionAdapter」を作ってみました。
BaseSectionAdapter.java
package jp.classmethod; import java.util.ArrayList; import java.util.List; import android.content.Context; import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.TextView; public class BaseSectionAdapter<T1, T2> extends BaseAdapter { /** インデックス行:ヘッダー */ private static final int INDEX_PATH_ROW_HEADER = -1; /** ビュータイプ:ヘッダー行 */ private static final int ITEM_VIEW_TYPE_HEADER = 0; /** ビュータイプ:データ行 */ private static final int ITEM_VIEW_TYPE_ROW = 1; protected Context context; protected LayoutInflater inflater; /** ヘッダー行で使用するデータリスト */ protected List<T1> sectionList; /** データ行で使用するデータリスト */ protected List<List<T2>> rowList; private List<IndexPath> indexPathList; public BaseSectionAdapter(Context context, List<T1> sectionList, List<List<T2>> rowList) { super(); this.context = context; this.inflater = LayoutInflater.from(context); this.sectionList = sectionList; this.rowList = rowList; this.indexPathList = getIndexPathList(sectionList, rowList); } @Override public int getCount() { int count = indexPathList.size(); return count; } @Override public Object getItem(int position) { IndexPath indexPath = indexPathList.get(position); if (isHeader(indexPath)) { return sectionList.get(indexPath.section); } else { return rowList.get(indexPath.section).get(indexPath.row); } } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { IndexPath indexPath = indexPathList.get(position); // ヘッダー行とデータ行とで分岐します。 if (isHeader(indexPath)) { return viewForHeaderInSection(convertView, indexPath.section); } else { return cellForRowAtIndexPath(convertView, indexPath); } } /** * ヘッダー行のViewを返します。 * * @param convertView * @param section * @return ヘッダー行のView */ public View viewForHeaderInSection(View convertView, int section) { if (convertView == null) { convertView = inflater.inflate(android.R.layout.simple_list_item_1, null); TextView castedConvertView = (TextView) convertView; castedConvertView.setBackgroundColor(Color.GRAY); castedConvertView.setTextColor(Color.WHITE); } TextView textView = (TextView) convertView; textView.setText(sectionList.get(section).toString()); return convertView; } /** * データ行のViewを返します。 * * @param convertView * @param indexPath * @return データ行のView */ public View cellForRowAtIndexPath(View convertView, IndexPath indexPath) { if (convertView == null) { convertView = inflater.inflate(android.R.layout.simple_list_item_1, null); } TextView textView = (TextView) convertView; textView.setText(rowList.get(indexPath.section).get(indexPath.row).toString()); return convertView; } @Override public int getViewTypeCount() { // ヘッダー行とデータ行の2種類なので、2を返します。 return 2; } @Override public int getItemViewType(int position) { // ビュータイプを返します。 if (isHeader(position)) { return ITEM_VIEW_TYPE_HEADER; } else { return ITEM_VIEW_TYPE_ROW; } } @Override public boolean isEnabled(int position) { if (isHeader(position)) { // ヘッダー行の場合は、タップできないようにします。 return false; } else { return super.isEnabled(position); } } /** * インデックスパスリストを取得します。 * * @param sectionList * @param rowList * @return インデックスパスリスト */ private List<IndexPath> getIndexPathList(List<T1> sectionList, List<List<T2>> rowList) { List<IndexPath> indexPathList = new ArrayList<IndexPath>(); for (int i = 0; i < sectionList.size(); i++) { IndexPath sectionIndexPath = new IndexPath(); sectionIndexPath.section = i; sectionIndexPath.row = INDEX_PATH_ROW_HEADER; indexPathList.add(sectionIndexPath); List<T2> rowListBySection = rowList.get(i); for (int j = 0; j < rowListBySection.size(); j++) { IndexPath rowIndexPath = new IndexPath(); rowIndexPath.section = i; rowIndexPath.row = j; indexPathList.add(rowIndexPath); } } return indexPathList; } private boolean isHeader(int position) { IndexPath indexPath = indexPathList.get(position); return isHeader(indexPath); } private boolean isHeader(IndexPath indexPath) { if (INDEX_PATH_ROW_HEADER == indexPath.row) { return true; } else { return false; } } } [/java] <p>ごっそり、ソースを載せてしまいましたが、大事なポイントだけ解説します。</p> <ul> <li>L14「T1」について:ヘッダー行に表示するデータを格納するモデルクラスの仮型引数です。使うときには具体的なクラスを指定します。</li> <li>L14「T2」について:データ行に表示するデータを格納するモデルクラスの仮型引数です。使うときには具体的なクラスを指定します。</li> <li>L66~L76 getViewメソッド:引数のpositionをもとに、ビュー取得処理をヘッダー行用とデータ行用に分岐しています。</li> <li>L113~L117 getViewTypeCountメソッド:ビュータイプの数を返しています。今回は、ヘッダー行とデータ行の2種類なので「2」を返しています。</li> <li>L119~L127 getItemViewTypeメソッド:引数のpositionがヘッダーかどうかを判別し、それに対応したビュータイプを返しています。</li> </ul> <p>getViewTypeCountメソッド、getItemViewTypeメソッドをオーバーライドして実装することにより、それぞれのビュータイプごとにセルが再利用されるようになります。<br />再利用可能なセルがある場合は、getViewメソッドが呼び出されるときに引数のconvertViewにそれぞれのビュータイプのセルのインスタンスがセットされますので、あとはビュータイプごとの処理を行うだけになります。</p> <h2 id="toc-">使い方</h2> <p>次に、先ほど作成した「BaseSectionAdapter」の使い方を紹介します。ヘッダー行、データ行にはカスタムレイアウトを使用しています。</p> <h4>手順1:インポート</h4> <p>「BaseSectionAdapter」クラス、[IndexPath]クラスを使用できるようにプロジェクト内に配置します。</p> <h4>手順2:ヘッダー行とデータ行の部品の作成</h4> <p>まず、ヘッダー行のレイアウトを作ります。<br />section_list_header.xml</p> <br /> <p>次に、ヘッダー行に表示するデータを格納するモデルクラスを作ります。<br />SectionHeaderData.java</p> package jp.classmethod; public class SectionHeaderData { public SectionHeaderData(String title, String subTitle) { this.title = title; this.subTitle = subTitle; } public String title; public String subTitle; }
データ行も同じように、レイアウトとモデルクラスを作ります。
section_list_row.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="50dp" android:gravity="center_vertical" android:minHeight="50dp" android:paddingLeft="10dp" android:paddingRight="10dp" > <TextView android:id="@+id/labelTxt" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_alignParentLeft="true" /> <TextView android:id="@+id/valueTxt" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_alignParentRight="true" android:layout_toRightOf="@+id/labelTxt" android:gravity="right" /> </RelativeLayout>
SectionRowData.java
package jp.classmethod; public class SectionRowData { public SectionRowData(String label, Integer value) { this.label = label; this.value = value; } public String label; public Integer value; }
手順3:「BaseSectionAdapter」のカスタマイズ
カスタムレイアウトを作成しましたので、それを使うためのアダプタークラスも BaseSectionAdapterクラスを継承してカスタマイズします。
CustomSectionListAdapter.java
package jp.classmethod; import java.util.List; import android.content.Context; import android.view.View; import android.widget.TextView; public class CustomSectionListAdapter extends BaseSectionAdapter<SectionHeaderData, SectionRowData> { public CustomSectionListAdapter(Context context, List<SectionHeaderData> sectionList, List<List<SectionRowData>> rowList) { super(context, sectionList, rowList); } @Override public View viewForHeaderInSection(View convertView, int section) { ListHeaderViewHolder holder = null; if (convertView == null) { convertView = inflater.inflate(R.layout.section_list_header, null); holder = new ListHeaderViewHolder(); holder.titleTxt = (TextView) convertView.findViewById(R.id.titleTxt); holder.subtitleTxt = (TextView) convertView.findViewById(R.id.subtitleTxt); convertView.setTag(holder); } else { holder = (ListHeaderViewHolder) convertView.getTag(); } SectionHeaderData headerData = sectionList.get(section); holder.titleTxt.setText(headerData.title); holder.subtitleTxt.setText(headerData.subTitle); return convertView; } @Override public View cellForRowAtIndexPath(View convertView, IndexPath indexPath) { ListRowViewHolder holder = null; if (convertView == null) { convertView = inflater.inflate(R.layout.section_list_row, null); holder = new ListRowViewHolder(); holder.labelTxt = (TextView) convertView.findViewById(R.id.labelTxt); holder.valueTxt = (TextView) convertView.findViewById(R.id.valueTxt); convertView.setTag(holder); } else { holder = (ListRowViewHolder) convertView.getTag(); } SectionRowData rowData = rowList.get(indexPath.section).get(indexPath.row); holder.labelTxt.setText(rowData.label); holder.valueTxt.setText(rowData.value.toString()); return convertView; } static class ListHeaderViewHolder { TextView titleTxt; TextView subtitleTxt; } static class ListRowViewHolder { TextView labelTxt; TextView valueTxt; } }
- L9 SectionHeaderData:ヘッダー行で使用するモデルクラスを指定します。
- L9 SectionRowData:データ行で使用するモデルクラスを指定します。
- L11 List<SectionHeaderData> sectionList:Listのジェネリクスにヘッダー行で使用するモデルクラスを指定します。
- L12 List<List<SectionRowData>> rowList:Listのジェネリクスにデータ行で使用するモデルクラスを指定します。
- L16~32 viewForHeaderInSectionメソッド:ヘッダー行のセルを返します。convertViewにインスタンスが入っている場合は、再利用可能なので、そのまま使用します。このメソッドは、iOSの「tableView:viewForHeaderInSection:」メソッドと同じような役割を持たせています。
- L34~50 cellForRowAtIndexPathメソッド:データ行のセルを返します。convertViewにインスタンスが入っている場合は、再利用可能なので、そのまま使用します。このメソッドは、iOSの「tableView:cellForRowAtIndexPath:」メソッドと同じような役割を持たせています。
ちなみに、ヘッダー行とデータ行のそれぞれの処理で、ViewHolderを作成してtagに格納するようにしていますが、これは、再利用時に各TextViewのインスタンスを取得する「findViewById」メソッドの処理を省き、高速化するためです。
手順4:データのセットと動作確認
最後にデータをセットして動かしてみます。
SectionListFragment.java
package jp.classmethod; import java.util.ArrayList; import java.util.List; import android.os.Bundle; import android.support.v4.app.ListFragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; public class SectionListFragment extends ListFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.section_list_fragment, container, false); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); List<SectionHeaderData> sectionList = new ArrayList<SectionHeaderData>(); List<List<SectionRowData>> rowList = new ArrayList<List<SectionRowData>>(); sectionList.add(new SectionHeaderData("title01", "subtitle01")); List<SectionRowData> sectionDatalist01 = new ArrayList<SectionRowData>(); sectionDatalist01.add(new SectionRowData("section01_data01", 111)); sectionDatalist01.add(new SectionRowData("section01_data02", 222)); sectionDatalist01.add(new SectionRowData("section01_data03", 333)); rowList.add(sectionDatalist01); sectionList.add(new SectionHeaderData("title02", "subtitle02")); List<SectionRowData> sectionDatalist02 = new ArrayList<SectionRowData>(); sectionDatalist02.add(new SectionRowData("section02_data01", 444)); sectionDatalist02.add(new SectionRowData("section02_data02", 555)); sectionDatalist02.add(new SectionRowData("section02_data03", 666)); sectionDatalist02.add(new SectionRowData("section02_data04", 777)); rowList.add(sectionDatalist02); sectionList.add(new SectionHeaderData("title03", "subtitle03")); List<SectionRowData> sectionDatalist03 = new ArrayList<SectionRowData>(); sectionDatalist03.add(new SectionRowData("section03_data01", 888)); sectionDatalist03.add(new SectionRowData("section03_data02", 999)); sectionDatalist03.add(new SectionRowData("section03_data03", 1111)); sectionDatalist03.add(new SectionRowData("section03_data04", 2222)); sectionDatalist03.add(new SectionRowData("section03_data05", 3333)); rowList.add(sectionDatalist03); CustomSectionListAdapter adapter = new CustomSectionListAdapter(getActivity(), sectionList, rowList); setListAdapter(adapter); } }
- L16 R.layout.section_list_fragment:ここで使用している画面レイアウトの中身は割愛させていただきますが、「ListView」とempty用の「TextView」が置いてあるシンプルなものです。
- L23 new ArrayList<SectionHeaderData>():ヘッダー行のデータを格納するListを生成します。
- L24 new ArrayList<List<SectionRowData>>():データ行のデータを格納するListを生成します。やや分かり難いですが、ここで生成したListに各セクション毎のListを追加していくため、このような型になっています。
- L50 new CustomSectionListAdapter():コンテキストと、ヘッダー行のデータリストと、データ行のデータリストを、アダプターのコンストラクタに渡して、CustomSectionListAdapterインスタンスを生成しています。
では、やっと準備ができましたので実行してみます。
何とか動きました。。。
iOSのようにヘッダーを常に上部に残す処理、フッターがない、List<List<T2>>が気持ち悪いなど、いろいろ気になるところはありますが、とりあえず今回はここまでということで。
ではでは。